/* MIT License
 *
 * Copyright (c) 2012 Reuben Smith, Miami University
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package org.droidlike.vsig;


import com.jme3.app.Application;
import com.jme3.app.state.AbstractAppState;
import com.jme3.app.state.AppStateManager;
import com.jme3.input.InputManager;
import com.jme3.input.MouseInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.AnalogListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.MouseAxisTrigger;
import com.jme3.math.FastMath;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import java.util.logging.Level;
import java.util.logging.Logger;


/**
 * SimulationCamera.
 *
 * @author smithre5
 */
public class SimulationCamera
        extends AbstractAppState
{
    public SimulationCamera(Simulation sim, Vector3f[] bounds)
    {
        this.sim = sim;
        
        deadzone = DEADZONE;
        move_rate = MOVE_RATE;
        view_angle = VIEW_ANGLE;
        view_distance_far = VIEW_DISTANCE_FAR;
        view_rate_pitch = VIEW_RATE_PITCH;
        view_rate_yaw = VIEW_RATE_YAW;
        zoom_amount = ZOOM_AMOUNT;
        
        if (bounds == null) {
            bounds = new Vector3f[]{
                new Vector3f(Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY),
                new Vector3f(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY)
            };
            
            Logger.getLogger(VSIGFrame.class.getName()).log(Level.WARNING, null, 
                "Simulation camera given null bounds.");
        }
        
        cameraMoveBounds = bounds;
        cameraLookBounds = VIEW_BOUNDS;
    }
    

    @Override
    public void initialize(AppStateManager stateManager, Application app)
    {
        super.initialize(stateManager, app);
        
        Profile profile = VSIG.getProfile();
        deadzone = Float.parseFloat(profile.getValue("camera.deadzone"));
        deadzone = deadzone * deadzone;
        move_rate = Float.parseFloat(profile.getValue("camera.move_rate"));
        view_angle = Float.parseFloat(profile.getValue("camera.view_angle"));
        view_distance_far = Float.parseFloat(profile.getValue("camera.view_distance"));
        view_rate_pitch = Float.parseFloat(profile.getValue("camera.view_rate_pitch")) * FastMath.DEG_TO_RAD;
        view_rate_yaw = Float.parseFloat(profile.getValue("camera.view_rate_yaw")) * FastMath.DEG_TO_RAD;
        zoom_amount = (1.0f / (Integer.parseInt(profile.getValue("camera.zoom_levels")) - 1)) * (World.BOUNDS[1].y - World.BOUNDS[0].y);

        camera = app.getCamera();
        camera.setLocation(new Vector3f(0, World.BOUNDS[0].y, World.BOUNDS[0].y));
        camera.lookAt(Vector3f.ZERO, Vector3f.UNIT_Y);
        
        input = app.getInputManager();
        
        updatePerspective();
        
        addInputs();
    }


    @Override
    public void update(float tpf)
    {
        // TODO Make move/look speed scale with distance from center.
        // TODO Make move work based off of look dir.
        
        Vector3f camPos = camera.getLocation();
        float[] camRot = camera.getRotation().toAngles(new float[3]);
        
        Vector2f cursorPos = input.getCursorPosition();
        Vector2f screenCenter = new Vector2f(camera.getWidth() / 2, camera.getHeight() / 2);
            
        if (cursorPos.distanceSquared(screenCenter) > deadzone) {
            Vector2f cursorOffset = new Vector2f(
                cursorPos.x - screenCenter.x,
                screenCenter.y - cursorPos.y
            ).normalizeLocal();
            
            if (cameraLooking) {
                camRot[0] += cursorOffset.y * view_rate_pitch * tpf;
                camRot[1] -= cursorOffset.x * view_rate_yaw * tpf;
            }
            else if (cameraMoving) {
                Vector3f camForward = camera.getDirection().clone().multLocal(1, 0, 1).normalizeLocal();
                Vector3f camRight = camForward.cross(Vector3f.UNIT_Y);
                
                cursorOffset.multLocal((camPos.y / World.BOUNDS[0].y) * move_rate * tpf);
                camForward.multLocal(cursorOffset.y);
                camRight.multLocal(cursorOffset.x);
                
                camPos.subtractLocal(camForward.subtractLocal(camRight));
            }
        }
                
        if (!VSIG.getDebugMode()) {
            camPos.x = Util.clamp(camPos.x, cameraMoveBounds[0].x, cameraMoveBounds[1].x);
            camPos.y = Util.clamp(camPos.y, cameraMoveBounds[0].y, cameraMoveBounds[1].y);
            camPos.z = Util.clamp(camPos.z, cameraMoveBounds[0].z, cameraMoveBounds[1].z);
            
            camRot[0] = Util.clamp(camRot[0], cameraLookBounds[0][0], cameraLookBounds[1][0]);
            camRot[1] = Util.clamp(camRot[1], cameraLookBounds[0][1], cameraLookBounds[1][1]);
            camRot[2] = Util.clamp(camRot[2], cameraLookBounds[0][2], cameraLookBounds[1][2]);
        }
        
        camera.setLocation(camPos);
        camera.setRotation(camera.getRotation().fromAngles(camRot));
    }
    
    
    /**
     * Updates camera perspective: field-of-view, view ratio, and near and far
     * clip planes. (This should probably be parametric rather than entirely
     * constants.)
     */
    public void updatePerspective()
    {        
        camera.setFrustumPerspective(
                view_angle,
                (float)camera.getWidth() / camera.getHeight(),
                VIEW_DISTANCE_NEAR,
                view_distance_far);

        camera.update();
    }


    @Override
    public void cleanup()
    {
        super.cleanup();
    }
    
    
    /**
     * Adds input mappings and listener to the input manager.
     */
    private void addInputs()
    {
        // smithre5: Might be an issue with using LSHIFT for CamLook since jME
        // seems to glom all input.
        
        Profile profile = VSIG.getProfile();
        
        input.addMapping("ToggleCameraMove", new KeyTrigger(
            Integer.parseInt(profile.getValue("input.camera_move"))));
        input.addMapping("ToggleCameraLook", new KeyTrigger(
            Integer.parseInt(profile.getValue("input.camera_view"))));
        input.addMapping("CameraMoveY+", new MouseAxisTrigger(MouseInput.AXIS_WHEEL, false));
        input.addMapping("CameraMoveY-", new MouseAxisTrigger(MouseInput.AXIS_WHEEL, true));
        input.addMapping("CameraLookReset", new KeyTrigger(
            Integer.parseInt(profile.getValue("input.camera_reset"))));
        
        input.addListener(new ActionListener() {
            public void onAction(String name, boolean pressed, float tpf)
            {
                if (name.startsWith("Toggle")) {
                    if (name.endsWith("CameraMove")) {
                        cameraMoving = pressed;
                    }
                    else if (name.endsWith("CameraLook")) {
                        cameraLooking = pressed;
                    }
                }
                else if (name.equals("CameraLookReset")) {
                    Vector3f camPos = new Vector3f(camera.getLocation());
                    camera.lookAt(camPos.addLocal(0, -camPos.y, -camPos.y), Vector3f.UNIT_Y);
                }
            }
        }, new String[]{
            "ToggleCameraMove",
            "ToggleCameraLook",
            "CameraLookReset"
        });
        
        input.addListener(new AnalogListener() {
            public void onAnalog(String name, float value, float tpf)
            {
                if (cameraMoving) {
                    Vector3f camPos = camera.getLocation();
                    
                    if (name.equals("CameraMoveY+")) {
                        camPos.y += zoom_amount * value;
                    }
                    else if (name.equals("CameraMoveY-")) {
                        camPos.y -= zoom_amount * value;
                    }
                }
            }
        }, new String[]{"CameraMoveY+", "CameraMoveY-"});
    }

    public static final float DEADZONE =     // px^2
        150.0f * 150.0f;
    public static final float MOVE_RATE =         // wu/s
        100.0f;
    /*
     * FOV should remain around [70, 90] degrees. Possibly move to profile
     * settings for customization. Maybe not; too many settings might confuse
     * users.
     */
    public static final float VIEW_ANGLE = 75.0f;
    public static final float[][] VIEW_BOUNDS = new float[][]{
        // {pitch, yaw, roll} radians
        {              0f, -FastMath.TWO_PI,   0f},
        {FastMath.HALF_PI,  FastMath.TWO_PI,   0f}
    };
    /*
     * Near frustum should remain <= 0.01f to limit objects disappearing from
     * getting too close to the camera. Far frustum should probably be moved
     * to profile settings so that the user can adjust it based on system
     * performance.
     */
    public static final float VIEW_DISTANCE_NEAR = 0.01f;
    public static final float VIEW_DISTANCE_FAR = 10560.0f;
    public static final float VIEW_RATE_PITCH =    // deg/s
        120.0f * FastMath.DEG_TO_RAD;
    public static final float VIEW_RATE_YAW =      // deg/s
        120.0f * FastMath.DEG_TO_RAD;
    public static final int ZOOM_LEVELS =
        20;
    public static final float ZOOM_AMOUNT =
        (1.0f / (ZOOM_LEVELS - 1)) * (World.BOUNDS[1].y - World.BOUNDS[0].y);
   
    private Simulation sim;
    
    private InputManager input;
    
    private float deadzone;
    private float move_rate;
    private float view_angle;
    private float view_distance_far;
    private float view_rate_pitch;
    private float view_rate_yaw;
    private float zoom_amount;
    
    private Camera camera;
    private Vector3f[] cameraMoveBounds;
    private float[][] cameraLookBounds;
    private boolean cameraMoving;
    private boolean cameraLooking;
}

